/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include <cstdlib>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <iostream>
#include <sstream>

#include <exception>
#include <unistd.h>

#include "KAssert.h"
#include "Exceptions.h"
#include "ExecFormat.h"
#include "Memory.h"
#include <std_support/Atomic.hpp>
#include "concurrent/Mutex.hpp"
#include "Porting.h"
#include "Types.h"
#include "Utils.hpp"
#include "ObjCExceptions.h"
#include "BuildIdUtils.h"

// region Tencent Code
#ifdef KONAN_OHOS
// TODO: When ohos dfx raises length limit on set_fatal_message, print the full kotlin stack trace instead
// of just the addresses.

#include "Natives.h"
#include <dlfcn.h>

extern "C" OBJ_GETTER(Kotlin_Throwable_getStackTrace, KRef throwable);
extern "C" __attribute__((weak)) void set_fatal_message(const char* msg);

std::string concatFatalMessage(std::vector<std::string> soFiles, std::vector<std::string> addresses, std::string message) {
  std::string soFilesStr;
  std::string addressesStr;
  std::string preIndex;
  std::string addressLine;
  for (size_t i = 0; i < addresses.size(); ++i) {
    std::string index = addresses[i].substr(0, addresses[i].find(' '));
    std::string address = addresses[i].substr(addresses[i].find(' ') + 1);
    std::string soInfo = soFiles[std::stoi(index)];
    std::string soFileStrTmp;
    std::string addressTmp;

    if (soFilesStr.find(soInfo) == std::string::npos) {
        soFileStrTmp = soInfo;
    }

    if (preIndex.empty() || preIndex != index) {
        preIndex = index;
        addressTmp = "\n[" + index + "] " + address;
    } else {
        addressTmp = " " + address;
    }
    if (message.length() + soFilesStr.length() + addressLine.length() + soFileStrTmp.length() + addressTmp.length() + 22 < 1004) {
        soFilesStr += soFilesStr == "" ? soFileStrTmp : soFileStrTmp == "" ? "" : "," + soFileStrTmp;
        addressLine += addressTmp;
    } else {
        break;
    }
  }

  addressesStr += addressLine.empty() ? "" : addressLine;
  return message + "sofiles:\n" + soFilesStr + "\n" + "addresses:" + addressesStr;
}

void ReportBacktraceToOhosLog(KRef exception) {
  if (set_fatal_message == nullptr) return;

  ObjHolder stackTraceHolder;
  ArrayHeader* stackTrace = Kotlin_Throwable_getStackTrace(exception, stackTraceHolder.slot())->array();
  Dl_info info;

  pid_t pid = getpid();
  std::vector<MapsEntry> mapCache = BuildIdUtils::parseMapsFile(pid);

  std::string message = "\nUncaught Kotlin exception:\n";
  std::vector<std::string> soFiles;
  std::vector<std::string> addresses;

  for (uint32_t index = 0; index < stackTrace->count_; ++index) {
      KNativePtr ptr = *PrimitiveArrayAddressOfElementAt<KNativePtr>(stackTrace, index);
      if (dladdr(ptr, &info) != 0) {
        std::vector<uint8_t> buildId;
        std::string soPath = BuildIdUtils::findSoPathFromMaps(reinterpret_cast<uintptr_t>(ptr), mapCache);

        size_t lastSlashPos = soPath.find_last_of("/");

        std::string soFileName = soPath.substr(lastSlashPos + 1);
        std::string buildIdStr = BuildIdUtils::getSoBuildId(soPath, buildId);
        std::string soInfo = buildIdStr.empty() ? soFileName : soFileName + "(" + buildIdStr + ")";

        uintptr_t offset = reinterpret_cast<uintptr_t>(ptr) - reinterpret_cast<uintptr_t>(info.dli_fbase) - 1;
        std::string addr = "";
        std::stringstream ss;
        ss << std::hex << offset;
        addr = "0x" + ss.str();

        int index = -1;
        for (size_t i = 0; i < soFiles.size(); i++) {
            if (soFiles[i] == soInfo) {
                index = static_cast<int>(i);
                break;
            }
        }

        if (index == -1) {
            soFiles.push_back(soInfo);
            index = soFiles.size() - 1;
        }
        addresses.push_back(std::to_string(index) + " " + addr);
      }
    }
  std::string fatalMessage = concatFatalMessage(soFiles, addresses, message);
  set_fatal_message(fatalMessage.substr(0, 1004).c_str());
}

#endif
// endregion

// Defined in RuntimeUtils.kt
extern "C" void Kotlin_runUnhandledExceptionHook(KRef exception);
extern "C" void ReportUnhandledException(KRef exception);

void ThrowException(KRef exception) {
  RuntimeAssert(exception != nullptr && IsInstanceInternal(exception, theThrowableTypeInfo),
                "Throwing something non-throwable");
  ExceptionObjHolder::Throw(exception);
}

void HandleCurrentExceptionWhenLeavingKotlinCode() {
  try {
      std::rethrow_exception(std::current_exception());
  } catch (ExceptionObjHolder& e) {
      std::terminate();  // Terminate when it's a kotlin exception.
  }
}

namespace {

class {
    /**
     * Timeout 5 sec for concurrent (second) terminate attempt to give a chance the first one to finish.
     * If the terminate handler hangs for 5 sec it is probably fatally broken, so let's do abnormal _Exit in that case.
     */
    unsigned int timeoutSec = 5;
    std::atomic<int> terminatingFlag = 0;
  public:
    template <class Fun> RUNTIME_NORETURN void operator()(Fun block) {
      if (kotlin::std_support::atomic_compare_exchange_strong(terminatingFlag, 0, 1)) {
        block();
        // block() is supposed to be NORETURN, otherwise go to normal abort()
        std::abort();
      } else {
        kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
        sleep(timeoutSec);
        // We come here when another terminate handler hangs for 5 sec, that looks fatally broken. Go to forced exit now.
      }
      _Exit(EXIT_FAILURE); // force exit
    }
} concurrentTerminateWrapper;

void RUNTIME_NORETURN terminateWithUnhandledException(KRef exception) {
    kotlin::AssertThreadState(kotlin::ThreadState::kRunnable);
    concurrentTerminateWrapper([exception]() {
        ReportUnhandledException(exception);
#if KONAN_REPORT_BACKTRACE_TO_IOS_CRASH_LOG
        ReportBacktraceToIosCrashLog(exception);
#endif
// region Tencent Code
#ifdef KONAN_OHOS
        ReportBacktraceToOhosLog(exception);
#endif
// endregion

        // Best effort to make sure the reported exception gets actually printed:
        konan::consoleFlush();

        std::abort();
    });
}

void processUnhandledException(KRef exception) noexcept {
    kotlin::AssertThreadState(kotlin::ThreadState::kRunnable);
    try {
        Kotlin_runUnhandledExceptionHook(exception);
    } catch (ExceptionObjHolder& e) {
        terminateWithUnhandledException(e.GetExceptionObject());
    }
}

} // namespace

PERFORMANCE_INLINE RUNTIME_NOTHROW OBJ_GETTER(Kotlin_getExceptionObject, void* holder) {
    RETURN_OBJ(static_cast<ExceptionObjHolder*>(holder)->GetExceptionObject());
}

namespace {
// Copy, move and assign would be safe, but not much useful, so let's delete all (rule of 5)
class TerminateHandler : private kotlin::Pinned {
  RUNTIME_NORETURN static void queuedHandler() {
      concurrentTerminateWrapper([]() {
          // Not a Kotlin exception - call default handler
          instance().queuedHandler_();
      });
  }

  // In fact, it's safe to call my_handler directly from outside: it will do the job and then invoke original handler,
  // even if it has not been initialized yet. So one may want to make it public and/or not the class member
  RUNTIME_NORETURN static void kotlinHandler() {
      if (auto currentException = std::current_exception()) {
        try {
          std::rethrow_exception(currentException);
        } catch (ExceptionObjHolder& e) {
          // Both thread states are allowed here because there is no guarantee that
          // C++ runtime will unwind the stack for an unhandled exception. Thus there
          // is no guarantee that state switches made on interop borders will be rolled back.

          // Moreover, a native code can catch an exception thrown by a Kotlin callback,
          // store it to a global and then re-throw it in another thread which is not attached
          // to the Kotlin runtime. To handle this case, use the CalledFromNativeGuard.
          // TODO: Forbid throwing Kotlin exceptions through the interop border to get rid of this case.
          kotlin::CalledFromNativeGuard guard(/* reentrant = */ true);
          processUnhandledException(e.GetExceptionObject());
          terminateWithUnhandledException(e.GetExceptionObject());
        } catch (...) {
          // Not a Kotlin exception - call default handler
          kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
          queuedHandler();
        }
      }
      // Come here in case of direct terminate() call or unknown exception - go to default terminate handler.
      kotlin::NativeOrUnregisteredThreadGuard guard(/* reentrant = */ true);
      queuedHandler();
  }

  using QH = __attribute__((noreturn)) void(*)();
  QH queuedHandler_;

  /// Use machinery like Meyers singleton to provide thread safety
  TerminateHandler()
    : queuedHandler_((QH)std::set_terminate(kotlinHandler)) {}

  static TerminateHandler& instance() {
    static TerminateHandler singleton [[clang::no_destroy]];
    return singleton;
  }

  // Dtor might be in use to restore original handler. However, consequent install
  // will not reconstruct handler anyway, so let's keep dtor deleted to avoid confusion.
  ~TerminateHandler() = delete;
public:
  /// First call will do the job, all consequent will do nothing.
  static void install() {
    instance(); // Use side effect of warming up
  }
};
} // anon namespace

// Use one public function to limit access to the class declaration
void SetKonanTerminateHandler() {
  TerminateHandler::install();
}

extern "C" void RUNTIME_NORETURN Kotlin_terminateWithUnhandledException(KRef exception) {
    kotlin::AssertThreadState(kotlin::ThreadState::kRunnable);
    terminateWithUnhandledException(exception);
}

extern "C" void Kotlin_processUnhandledException(KRef exception) {
    kotlin::AssertThreadState(kotlin::ThreadState::kRunnable);
    processUnhandledException(exception);
}

void kotlin::ProcessUnhandledException(KRef exception) noexcept {
    // This may be called from any state, do reentrant state switch to runnable.
    kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true);
    processUnhandledException(exception);
}

void RUNTIME_NORETURN kotlin::TerminateWithUnhandledException(KRef exception) noexcept {
    // This may be called from any state, do reentrant state switch to runnable.
    kotlin::ThreadStateGuard guard(kotlin::ThreadState::kRunnable, /* reentrant = */ true);
    terminateWithUnhandledException(exception);
}
